package com.bubblegumproject.ogo.component;

import com.bubblegumproject.ogo.action.Context;
import com.bubblegumproject.ogo.component.injection.DependencyInjectorChain;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.log4j.Logger;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Proxy a class of Components to provide special services to method invocations on the methods.
 *
 * @author Azubuko Obele (buko.obele@gmail.com)
 */
public class ComponentProxy<T> implements MethodInterceptor {

    private Class<T> componentClazz;

    private Map<Method, ComponentInvocation> invocationMap = new HashMap<Method, ComponentInvocation>();

    private List<ComponentInvocation> startMethods = new ArrayList<ComponentInvocation>();

    private List<ComponentInvocation> stopMethods = new ArrayList<ComponentInvocation>();

    private List<DependentComponent> dependents = new ArrayList<DependentComponent>();

    private DependencyInjectorChain dependencyInjector;

    private static final Logger log = Logger.getLogger(ComponentProxy.class);

    public ComponentProxy(Class<T> componentClazz, DependencyInjectorChain injector) {
        assert (componentClazz != null);
        this.componentClazz = componentClazz;
        this.dependencyInjector = injector;

        // create the component invocations needed to do special invocations
        for (Method method : componentClazz.getMethods()) {
            if (method.isAnnotationPresent(ActionHandler.class)) {
                ActionHandlerComponentInvocation invocation = new ActionHandlerComponentInvocation(method);
                invocationMap.put(method, invocation);
                log.info(this + " created invocation " + invocation + " for method " + method);
            } else if (method.isAnnotationPresent(PostConstruct.class)) {
                startMethods.add(new StartMethodComponentInvocation(method));
            } else if (method.isAnnotationPresent(PreDestroy.class)) {
                stopMethods.add(new StopMethodComponentInvocation(method));
            } else {
                // even if it's not a special method, see if it has any interceptors attached to it and put a generic
                // component invocation on the method
                for (Annotation annotation : method.getAnnotations())  {
                    if (annotation.annotationType().isAnnotationPresent(InterceptorClass.class)) {
                        invocationMap.put(method, new ComponentInvocation(method));
                    }
                }
            }
        }

        // record any dependents a Component may have
        for (Method method : componentClazz.getMethods()) {
            if (method.isAnnotationPresent(Dependency.class)) {
                dependents.add(new DependentComponent(new Setter(method)));
            }
        }
    }

    public Class<T> getComponentClass() {
        return componentClazz;
    }

    public Collection<ComponentInvocation> getComponentInvocations() {
        return invocationMap.values();
    }

    public <U extends ComponentInvocation> Collection<U> getComponentInvocations(Class<U> clazz) {
        List<U> results = new ArrayList<U>();
        for (ComponentInvocation invocation : invocationMap.values())  {
            if (clazz.isInstance(invocation)) results.add((U) invocation);
        }
        return results;
    }

    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // if the method has a special (non-lifecycle) component invocation on it then invoke the invocation,
        // otherwise just invoke the plain method
        ComponentInvocation componentInvocation = invocationMap.get(method);
        if (componentInvocation != null) {
            return componentInvocation.invoke(proxy, methodProxy, args);
        } else
            return methodProxy.invokeSuper(proxy, args);
    }

    /**
     * @return new instance of the Component that will be provided its required component services
     */
    public T create() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(componentClazz);
        enhancer.setCallback(new ComponentInstanceProxy(this));
        enhancer.setInterfaces(new Class[] { ComponentInstance.class });
        return (T) enhancer.create();
    }

    public void start(Object component, Container container, Context context) throws Throwable {
        assert (component != null);
        // do dependency injection on the instance to set initial default values
        dependencyInjector.inject(component, componentClazz, context);

        // start and initialize any dependents on the component
        for (DependentComponent cd : dependents) {
            Object dependency = container.create(cd.getSetterMethod().getArgumentType(), context);
            cd.getSetterMethod().set(component,  dependency);
            ((ComponentInstance) component).addDependency(dependency);
        }

        // now invoke all start methods on the component
        for (ComponentInvocation invocation : startMethods) {
            invocation.invoke(component, (Object[]) null);
        }
    }

    public void stop(Object component, Container container) throws Throwable {
        assert (component != null);
        // stop all dependents on the component
        for (Object dependency : ((ComponentInstance) component).getDependencySequence()) {
            container.destroy(dependency);
        }

        // invoke all stop methods on the component
        for (ComponentInvocation invocation : stopMethods) {
            invocation.invoke(component, (Object[]) null);
        }
    }

    public String toString() {
        return getClass().getSimpleName() + "[componentClazz=" + componentClazz + ", invocationMap=" + invocationMap +
                ", startMethods=" + startMethods + ", stopMethods=" + stopMethods + "]";
    }
}
